import {
  YBMICRO_TIPS_REPEAT_RENDER,
  YBMICRO_DATA_FLAG,
  YBMICRO_SCRIPT_ID,
  YBMICRO_TIPS_NO_SCRIPT
} from '../constant';
import {
  rawAppendChild,
  rawDocumentQuerySelector,
  rawHeadInsertBefore,
  rawElementRemoveChild,
  rawElementContains,
  rawBodyInsertBefore
} from '../constant/prototype';
import { getYbMicroById } from '../lib/sandboxCache';
import { ScriptObject } from '../types/template.type';
import { isHijackingTag, getCurUrl, isFunction } from '../utils';
import { warn } from '../utils/logger';
import {
  getTagFromScript,
  isScriptElement,
  setAttrsToElement,
  setTagToScript
} from '../utils/element';
import { getExternalStyleSheets, getExternalScripts } from './entry';
import { getCssLoader } from './plugin';
import { parseTagAttributes } from './template';
import { insertScriptToIframe, patchElementEffect } from './iframe';

/**
 * @Author: chen.long
 * @Descripttion: 自定义事件劫持
 */
function patchCustomEvent(
  e: CustomEvent,
  elementGetter: () => HTMLScriptElement | HTMLLinkElement | null
): CustomEvent {
  Object.defineProperties(e, {
    srcElement: {
      get: elementGetter
    },
    target: {
      get: elementGetter
    }
  });

  return e;
}

let dynamicScriptExecStack = Promise.resolve();
/**
 * @Author: chen.long
 * @Descripttion: 劫持Append和insert方法
 */
function rewriteAppendOrInsertChild(opts: {
  rawDOMAppendOrInsertBefore: <T extends Node>(
    newChild: T,
    refChild?: Node | null
  ) => T;
  ybmicroId: string;
}) {
  return function appendChildOrInsertBefore<T extends Node>(
    this: HTMLHeadElement | HTMLBodyElement,
    newChild: T,
    refChild?: Node | null
  ) {
    let element = newChild as any;
    const { rawDOMAppendOrInsertBefore, ybmicroId } = opts;
    const sandbox = getYbMicroById(ybmicroId);

    const { iframe, lifecycles, proxyLocation, plugins } = sandbox;

    if (!isHijackingTag(element.tagName) || !ybmicroId) {
      const res = rawDOMAppendOrInsertBefore.call(this, element, refChild) as T;
      patchElementEffect(element, iframe.contentWindow);
      return res;
    }

    const iframeDocument = iframe.contentDocument;
    const curUrl = getCurUrl(proxyLocation);

    // TODO 过滤可以开放
    if (element.tagName) {
      switch (element.tagName?.toUpperCase()) {
        case 'LINK': {
          const { href, rel, type } = element as HTMLLinkElement;
          const styleFlag =
            rel === 'stylesheet' ||
            type === 'text/css' ||
            href.endsWith('.css');
          // 非 stylesheet 不做处理
          if (!styleFlag) {
            const res = rawDOMAppendOrInsertBefore.call(
              this,
              element,
              refChild
            );
            return res;
          }
          // 排除css
          if (href) {
            getExternalStyleSheets(
              [
                {
                  src: href
                }
              ],
              lifecycles.loadError
            ).forEach(({ src, ignore, contentPromise }) =>
              contentPromise.then(
                content => {
                  // 处理 ignore 样式
                  const rawAttrs = parseTagAttributes(element.outerHTML);
                  if (ignore && src) {
                    // 忽略的元素应该直接把对应元素插入，而不是用新的 link 标签进行替代插入，保证 element 的上下文正常
                    rawDOMAppendOrInsertBefore.call(this, element, refChild);
                  } else {
                    // 记录js插入样式，子应用重新激活时恢复
                    const stylesheetElement =
                      iframeDocument.createElement('style');
                    // 处理css-loader插件
                    const cssLoader = getCssLoader({ plugins });
                    stylesheetElement.innerHTML = cssLoader(
                      content,
                      src,
                      curUrl
                    );
                    setAttrsToElement(stylesheetElement, rawAttrs);
                    rawDOMAppendOrInsertBefore.call(
                      this,
                      stylesheetElement,
                      refChild
                    );
                    manualInvokeElementEvent(element, 'load');
                  }
                  element = null;
                },
                () => {
                  manualInvokeElementEvent(element, 'error');
                  element = null;
                }
              )
            );
          }

          const comment = iframeDocument.createComment(
            `dynamic link ${href} replaced by ybmicro`
          );
          return rawDOMAppendOrInsertBefore.call(this, comment, refChild);
        }
        case 'STYLE': {
          const stylesheetElement: HTMLStyleElement = newChild as any;
          const content = stylesheetElement.innerHTML;
          const cssLoader = getCssLoader({ plugins });
          content &&
            (stylesheetElement.innerHTML = cssLoader(content, '', curUrl));
          const res = rawDOMAppendOrInsertBefore.call(this, element, refChild);
          return res;
        }
        case 'SCRIPT': {
          setTagToScript(element);
          const { src, text, type, crossOrigin } = element as HTMLScriptElement;
          // 排除js
          if (src) {
            const execScript = (scriptResult: ScriptObject) => {
              // 假如子应用被连续渲染两次，两次渲染会导致处理流程的交叉污染
              if (sandbox.iframe === null)
                return warn(YBMICRO_TIPS_REPEAT_RENDER);
              const onload = () => {
                manualInvokeElementEvent(element, 'load');
                element = null;
              };
              insertScriptToIframe(
                { ...scriptResult, onload },
                sandbox.iframe.contentWindow,
                element
              );
            };
            const scriptOptions = {
              src,
              module: type === 'module',
              crossorigin: crossOrigin !== null,
              crossoriginType: crossOrigin || '',
              attrs: parseTagAttributes(element.outerHTML)
            } as ScriptObject;
            getExternalScripts([scriptOptions], lifecycles.loadError).forEach(
              scriptResult => {
                dynamicScriptExecStack = dynamicScriptExecStack.then(() =>
                  scriptResult.contentPromise.then(
                    content => {
                      if (sandbox.execQueue === null)
                        return warn(YBMICRO_TIPS_REPEAT_RENDER);
                      const execQueueLength = sandbox.execQueue?.length;
                      sandbox.execQueue.push(() =>
                        sandbox.requestIdleCallback(() => {
                          execScript({ ...scriptResult, content });
                        })
                      );
                      // 同步脚本如果都执行完了，需要手动触发执行
                      if (!execQueueLength) sandbox.execQueue.shift()();
                    },
                    () => {
                      manualInvokeElementEvent(element, 'error');
                      element = null;
                    }
                  )
                );
              }
            );
          } else {
            const execQueueLength = sandbox.execQueue?.length;
            sandbox.execQueue.push(() =>
              sandbox.requestIdleCallback(() => {
                insertScriptToIframe(
                  {
                    src: null,
                    content: text,
                    attrs: parseTagAttributes(element.outerHTML)
                  },
                  sandbox.iframe.contentWindow,
                  element
                );
              })
            );
            if (!execQueueLength) sandbox.execQueue.shift()();
          }
          // 内联脚本不会触发onload and onerror事件
          const comment = iframeDocument.createComment(
            `dynamic script ${src} replaced by ybmicro`
          );
          return rawDOMAppendOrInsertBefore.call(this, comment, refChild);
        }
        // 修正子应用内部iframe的window.parent指向
        case 'IFRAME': {
          // 嵌套的子应用的js-iframe需要插入子应用的js-iframe内部
          if (element.getAttribute(YBMICRO_DATA_FLAG) === '') {
            return rawAppendChild.call(
              rawDocumentQuerySelector.call(this.ownerDocument, 'html'),
              element
            );
          }
          const res = rawDOMAppendOrInsertBefore.call(this, element, refChild);
          return res;
        }
        default:
      }
    }
  };
}

/**
 * @Author: chen.long
 * @Descripttion: 手动触发事件回调
 */
function manualInvokeElementEvent(
  element: HTMLLinkElement | HTMLScriptElement,
  event: string
): void {
  const customEvent = new CustomEvent(event);
  const patchedEvent = patchCustomEvent(customEvent, () => element);
  if (isFunction(element[`on${event}`])) {
    element[`on${event}`](patchedEvent);
  } else {
    element.dispatchEvent(patchedEvent);
  }
}

function rewriteRemoveChild(opts: {
  rawElementRemoveChild: <T extends Node>(child: T) => T;
  ybmicroId: string;
}) {
  return function removeChild(child: Node) {
    const element = child as HTMLElement;
    const { rawElementRemoveChild, ybmicroId } = opts;
    if (element && isScriptElement(element)) {
      const { targetScript, iframe } = findScriptElementFromIframe(
        element as HTMLScriptElement,
        ybmicroId
      );
      if (targetScript !== null) {
        return iframe.contentWindow.__YBMICRO_RAW_DOCUMENT_HEAD__.removeChild(
          targetScript
        );
      }
      return null;
    }
    return rawElementRemoveChild(element);
  };
}

/**
 * @Author: chen.long
 * @Descripttion: 找出Script元素
 */
function findScriptElementFromIframe(
  rawElement: HTMLScriptElement,
  ybmicroId: string
) {
  const ybmicroTag = getTagFromScript(rawElement);
  const sandbox = getYbMicroById(ybmicroId);
  const { iframe } = sandbox;
  const targetScript =
    iframe.contentWindow.__YBMICRO_RAW_DOCUMENT_HEAD__.querySelector(
      `script[${YBMICRO_SCRIPT_ID}='${ybmicroTag}']`
    );
  if (targetScript === null) {
    warn(
      YBMICRO_TIPS_NO_SCRIPT,
      `<script ${YBMICRO_SCRIPT_ID}='${ybmicroTag}'/>`
    );
  }
  return { targetScript, iframe };
}

/**
 * @Author: chen.long
 * @Descripttion: 重写页面结构
 */
function rewriteContains(opts: {
  rawElementContains: (other: Node | null) => boolean;
  ybmicroId: string;
}) {
  return function contains(other: Node | null) {
    const element = other as HTMLElement;
    const { rawElementContains, ybmicroId } = opts;
    if (element && isScriptElement(element)) {
      const { targetScript } = findScriptElementFromIframe(
        element as HTMLScriptElement,
        ybmicroId
      );
      return targetScript !== null;
    }
    return rawElementContains(element);
  };
}

/**
 * @Author: chen.long
 * @Descripttion: 重写 head 和 body 劫持 appendChild 和 insertBefore
 */
export function patchRenderEffect(render: Document, id: string): void {
  render.head.appendChild = rewriteAppendOrInsertChild({
    rawDOMAppendOrInsertBefore: rawAppendChild,
    ybmicroId: id
  }) as typeof rawAppendChild;
  render.head.insertBefore = rewriteAppendOrInsertChild({
    rawDOMAppendOrInsertBefore: rawHeadInsertBefore as any,
    ybmicroId: id
  }) as typeof rawHeadInsertBefore;
  render.head.removeChild = rewriteRemoveChild({
    rawElementRemoveChild: rawElementRemoveChild.bind(render.head),
    ybmicroId: id
  }) as typeof rawElementRemoveChild;
  render.head.contains = rewriteContains({
    rawElementContains: rawElementContains.bind(render.head),
    ybmicroId: id
  }) as typeof rawElementContains;
  render.contains = rewriteContains({
    rawElementContains: rawElementContains.bind(render),
    ybmicroId: id
  }) as typeof rawElementContains;
  render.body.appendChild = rewriteAppendOrInsertChild({
    rawDOMAppendOrInsertBefore: rawAppendChild,
    ybmicroId: id
  }) as typeof rawAppendChild;
  render.body.insertBefore = rewriteAppendOrInsertChild({
    rawDOMAppendOrInsertBefore: rawBodyInsertBefore as any,
    ybmicroId: id
  }) as typeof rawBodyInsertBefore;
}
